/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.taketoday.context.cglib.proxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import cn.taketoday.context.cglib.core.CodeGenerationException;
import cn.taketoday.context.cglib.core.Signature;
import cn.taketoday.context.cglib.reflect.FastClass;

/**
 * Classes generated by {@link Enhancer} pass this object to the registered
 * {@link MethodInterceptor} objects when an intercepted method is invoked. It
 * can be used to either invoke the original method, or call the same method on
 * a different object of the same type.
 *
 * @author TODAY <br>
 * 2019-10-21 23:49
 */
public class MethodProxy {

  private final int i1;
  private final int i2;
  private final FastClass f1;
  private final FastClass f2;

  private final Signature sig1;
  private final Signature sig2;

  private MethodProxy(Signature sig1, Signature sig2, int i1, int i2, FastClass f1, FastClass f2) {
    this.sig1 = sig1;
    this.sig2 = sig2;
    this.f1 = f1;
    this.f2 = f2;
    this.i1 = i1;
    this.i2 = i2;
  }

  /**
   * For internal use by {@link Enhancer} only; see the
   * {@link cn.taketoday.context.cglib.reflect.FastMethod} class for similar
   * functionality.
   */
  public static MethodProxy create(Class<?> c1, Class<?> c2, String desc, String name1, String name2) {
    final Signature sig1 = new Signature(name1, desc);
    final Signature sig2 = new Signature(name2, desc);
    final FastClass f1 = FastClass.create(c1);
    final FastClass f2 = FastClass.create(c2);

    return new MethodProxy(sig1, sig2, f1.getIndex(sig1), f2.getIndex(sig2), f1, f2);
  }

  /**
   * Return the signature of the proxied method.
   */
  public Signature getSignature() {
    return sig1;
  }

  /**
   * Return the name of the synthetic method created by CGLIB which is used by
   * {@link #invokeSuper} to invoke the superclass (non-intercepted) method
   * implementation. The parameter types are the same as the proxied method.
   */
  public String getSuperName() {
    return sig2.getName();
  }

  /**
   * Return the {@link cn.taketoday.context.cglib.reflect.FastClass} method index
   * for the method used by {@link #invokeSuper}. This index uniquely identifies
   * the method within the generated proxy, and therefore can be useful to
   * reference external metadata.
   *
   * @see #getSuperName
   */
  public int getSuperIndex() {
    return i2;
  }

  // For testing
  FastClass getFastClass() {
    return f1;
  }

  // For testing
  FastClass getSuperFastClass() {
    return f2;
  }

  /**
   * Return the <code>MethodProxy</code> used when intercepting the method
   * matching the given signature.
   *
   * @param type
   *         the class generated by Enhancer
   * @param sig
   *         the signature to match
   *
   * @return the MethodProxy instance, or null if no applicable matching method is
   * found
   *
   * @throws IllegalArgumentException
   *         if the Class was not created by Enhancer or does not use a
   *         MethodInterceptor
   */
  public static MethodProxy find(Class<?> type, Signature sig) {
    try {
      Method m = type.getDeclaredMethod(MethodInterceptorGenerator.FIND_PROXY_NAME,
                                        MethodInterceptorGenerator.FIND_PROXY_TYPES);
      return (MethodProxy) m.invoke(null, new Object[] { sig });
    }
    catch (NoSuchMethodException e) {
      throw new IllegalArgumentException("Class " + type + " does not use a MethodInterceptor");
    }
    catch (IllegalAccessException e) {
      throw new CodeGenerationException(e);
    }
    catch (InvocationTargetException e) {
      throw new CodeGenerationException(e.getTargetException());
    }
  }

  /**
   * Invoke the original method, on a different object of the same type.
   *
   * @param obj
   *         the compatible object; recursion will result if you use the object
   *         passed as the first argument to the MethodInterceptor (usually not
   *         what you want)
   * @param args
   *         the arguments passed to the intercepted method; you may substitute
   *         a different argument array as long as the types are compatible
   *
   * @throws Throwable
   *         the bare exceptions thrown by the called method are passed
   *         through without wrapping in an
   *         <code>InvocationTargetException</code>
   * @see MethodInterceptor#intercept
   */
  public Object invoke(Object obj, Object[] args) throws Throwable {
    try {
      return f1.invoke(i1, obj, args);
    }
    catch (InvocationTargetException e) {
      throw e.getTargetException();
    }
    catch (IllegalArgumentException e) {
      if (i1 < 0) {
        throw new IllegalArgumentException("Protected method: " + sig1);
      }
      throw e;
    }
  }

  /**
   * Invoke the original (super) method on the specified object.
   *
   * @param obj
   *         the enhanced object, must be the object passed as the first
   *         argument to the MethodInterceptor
   * @param args
   *         the arguments passed to the intercepted method; you may substitute
   *         a different argument array as long as the types are compatible
   *
   * @throws Throwable
   *         the bare exceptions thrown by the called method are passed
   *         through without wrapping in an
   *         <code>InvocationTargetException</code>
   * @see MethodInterceptor#intercept
   */
  public Object invokeSuper(final Object obj, final Object[] args) throws Throwable {
    try {
      return f2.invoke(i2, obj, args);
    }
    catch (InvocationTargetException e) {
      throw e.getTargetException();
    }
  }
}
